package de.akabahn.ebd.stw.zdxfc.prs.tco;

import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.LAGE;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.SIGNAL;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.VERSCHLUSS;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.WEICHE;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.BGTASK_DELAY;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.BGTASK_INITIAL_DELAY;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.WINDOW_BG_COLOR;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.WINDOW_SIZE_HEIGHT;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.WINDOW_SIZE_WIDTH;
import static de.akabahn.ebd.stw.zdxfc.prs.tco.ZDXFCTcoConstants.WINDOW_TITLE;

import java.awt.BasicStroke;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import de.akabahn.ebd.dbd.DBDAdapter;
import de.akabahn.ebd.dbd.DBDAdapterFactory;
import de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants;
import de.akabahn.ebd.stw.zdxfc.prs.tco.elemente.Gleis;
import de.akabahn.ebd.stw.zdxfc.prs.tco.elemente.Lampe;
import de.akabahn.ebd.stw.zdxfc.prs.tco.elemente.Signal;
import de.akabahn.ebd.stw.zdxfc.prs.tco.elemente.StatischeElemente;
import de.akabahn.ebd.stw.zdxfc.prs.tco.elemente.Weiche;

/**
 * Kontrollklasse zum Starten und Stoppen des TCO, Schnittstelle zum DBD und Verwaltung 
 * aller Ausleuchtungs-Elemente.
 * 
 * @author Peter Eimann
 * @since 1.0
 * @version 1.1
 * @see DBDAdapter
 * @see ZDXFCTcoDispatcher
 * @see ZDXFCTcoInitializer
 * @see ZDXFCTcoConstants
 * @see ZDXFCConstants
 */
public class ZDXFCTcoMain extends Frame implements Observer {

	public final static String VERSION = "2.1.0_1";

	private static final long serialVersionUID = -6827257754622377241L;
	private static final Logger LOG = Logger.getLogger(ZDXFCTcoMain.class);
	
	private DBDAdapter dbd;
	private Map<String, Gleis> gleise;
	private Map<String, Weiche> weichen;
	private Map<String, Signal> signale;
	private Map<String, Lampe> lampen;
	private StatischeElemente statischeElemente;
	
	private ZDXFCTcoDispatcher dispatcher;
	private ZDXFCTcoInitializer initializer;
	private ScheduledExecutorService backgroundService;

	private boolean demoMode;
	private boolean sperreAktiv;

	/**
	 * Initialisierung der Klasse
	 * 
	 * @param demoMode <code>true</code> - Start der Anwendung im Demo-Mode ohne DBD-Anbindung 
	 */
	public ZDXFCTcoMain(boolean demoMode) {
		dbd = DBDAdapterFactory.getImplementation(this);
		gleise = new HashMap<String, Gleis>();
		weichen = new HashMap<String, Weiche>();
		signale = new HashMap<String, Signal>();
		lampen = new HashMap<String, Lampe>();
		statischeElemente = new StatischeElemente(this);
		
		dispatcher = new ZDXFCTcoDispatcher(this);
		initializer = new ZDXFCTcoInitializer(this, dispatcher);
		backgroundService = Executors.newSingleThreadScheduledExecutor();

		this.demoMode = demoMode;
		LOG.info("Running in DemoMode = " + demoMode);
	}
	
	/**
	 * Hoch- und geordnetes Runterfahren des TCO in der folgenden Reihenfolge:<br>
	 * <ol>
	 * <li>Starten des DBD-Adapters (wenn die Anwendung nicht im Demo-Mode laeuft)</li>
	 * <li>Initialisierung des Fensters</li>
	 * <li>Starten des Background-Tasks (Scheduling mit regelmaessiger Ausfuehrung)</li>
	 * </ol>
	 * @see DBDAdapter#start()
	 * @see ZDXFCTcoBackgroundTask#run()
	 * @see #initWindow()
	 */
	public void start() {
		if (!demoMode) {
			dbd.start();
		}

		initWindow();

		Runnable backgroundTask = new ZDXFCTcoBackgroundTask(this);
		backgroundService.scheduleWithFixedDelay(backgroundTask, BGTASK_INITIAL_DELAY, BGTASK_DELAY, TimeUnit.MILLISECONDS);

		System.out.println("Chateauxprix TCO up and running (Release " + VERSION + ")");
	}
	
	/**
	 * Direkter Aufruf von <code>paint(Graphics)</code> ohne Vorher den Inhalt des Fensters
	 * zu loeschen (vermeidet Flackern beim Neuzeichnen)
	 * 
	 * @see java.awt.Container#update(java.awt.Graphics)
	 * @see #paint(Graphics)
	 */
	@Override
	public void update(Graphics g) {
		paint(g);
	}

	/**
	 * Zeichnet alle Anzeigelemente innerhalb des Fensters mit ihren aktuellen Ausleuchtungseigenschaften
	 * 
	 * @see StatischeElemente#paint(Graphics)
	 * @see Signal#paint(Graphics)
	 * @see Gleis#paint(Graphics)
	 * @see Weiche#paint(Graphics)
	 * @see Lampe#paint(Graphics)
	 * @see java.awt.Window#paint(java.awt.Graphics)
	 */
	public void paint(Graphics g) {
		super.paint(g);
		
		Graphics2D g2 = (Graphics2D) g;
		g2.setStroke(new BasicStroke(1));

		statischeElemente.paint(g);
		
		for (String signalId : getSignale().keySet()) {
			if (signalId.endsWith("MS")) {
				getSignal(signalId).paint(g);
			}
		}

		g2.setStroke(new BasicStroke(4));
		for (String gleisId : getGleise().keySet()) {
			getGleis(gleisId).paint(g);
		}
		
		for (String weichenId : getWeichen().keySet()) {
			if (weichenId.endsWith("MS")) {
				getWeiche(weichenId).paint(g);
			}
		}
		
		for (String lampenId : getLampen().keySet()) {
			getLampe(lampenId).paint(g);
		}
	}
	
	/**
	 * Zugriff auf den DBD-Adapter
	 * 
	 * @return DBD-Adapter
	 */
	public DBDAdapter getDbd() {
		return dbd;
	}

	/**
	 * Zugriff auf alle Gleise
	 * 
	 * @return Gleise (Key ist die ID des Gleises)
	 */
	public Map<String, Gleis> getGleise() {
		return gleise;
	}
	
	/**
	 * Zugriff auf das Gleis mit der uebergebenen ID
	 * 
	 * @param id Die ID des Gleises
	 * @return Gleis mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Gleis getGleis(String id) {
		return getGleise().get(id);
	}
	
	/**
	 * Hinzufuegen eines Gleises zur Gleis-Map
	 * 
	 * @param gleis Das Gleis, der hinzugefuegt werden soll (als Key wird die ID des Gleises verwendet)
	 */
	public void addGleis(Gleis gleis) {
		getGleise().put(gleis.getId(), gleis);
	}
	
	/**
	 * Zugriff auf alle Weichen
	 * 
	 * @return Weiche (Key ist die ID der Weiche)
	 */
	public Map<String, Weiche> getWeichen() {
		return weichen;
	}
	
	/**
	 * Zugriff auf die Weiche mit der uebergebenen ID
	 * 
	 * @param id Die ID der Weiche
	 * @return Weiche mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Weiche getWeiche(String id) {
		return getWeichen().get(id);
	}
	
	/**
	 * Hinzufuegen einer Weiche zur Weichen-Map, die Weiche wird gleichzeitig mit der ID ihres Gleis- 
	 * und Lagemelders zur Map hinzugefuegt, damit sich Aenderungen bei beiden Meldern auf die gleiche
	 * Weiche auswirken.
	 * 
	 * @param weiche Die Weiche, die hinzugefuegt werden soll (als Key wird die ID der Weiche verwendet)
	 */
	public void addWeiche(Weiche weiche) {
		getWeichen().put(weiche.getId()[WEICHE], weiche);
		getWeichen().put(weiche.getId()[LAGE], weiche);
	}
	
	/**
	 * Zugriff auf alle Signale
	 * 
	 * @return Signal (Key ist die ID des Signals)
	 */
	public Map<String, Signal> getSignale() {
		return signale;
	}
	
	/**
	 * Zugriff auf das Signal mit der uebergebenen ID
	 * 
	 * @param id Die ID des Signals
	 * @return Signal mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Signal getSignal(String id) {
		return getSignale().get(id);
	}
	
	/**
	 * Hinzufuegen eines Signals zur Signal-Map, das Signal wird gleichzeitig mit der ID seines Halt- 
	 * und Verschlussmelders (falls vorhanden) zur Map hinzugefuegt, damit sich Aenderungen bei beiden
	 * Meldern auf das gleiche Signal auswirken.
	 * 
	 * @param signal Das Signal, die hinzugefuegt werden soll (als Key wird die ID des Signals verwendet)
	 */
	public void addSignal(Signal signal) {
		getSignale().put(signal.getId()[SIGNAL], signal);
		if (signal.getId()[VERSCHLUSS] != null) {
			getSignale().put(signal.getId()[VERSCHLUSS], signal);
		}
	}
	
	/**
	 * Zugriff auf alle Lampen
	 * 
	 * @return Lampe (Key ist die ID der Lampe)
	 */
	public Map<String, Lampe> getLampen() {
		return lampen;
	}
	
	/**
	 * Zugriff auf die Lampe mit der uebergebenen ID
	 * 
	 * @param id Die ID der Lampe
	 * @return Lampe mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Lampe getLampe(String id) {
		return getLampen().get(id);
	}
	
	/**
	 * Hinzufuegen einer Lampe zur Lampen-Map
	 * 
	 * @param lampe Die Lampe, die hinzugefuegt werden soll (als Key wird die ID der Lampe verwendet)
	 */
	public void addLampe(Lampe lampe) {
		getLampen().put(lampe.getId(), lampe);
	}
	
	/**
	 * Gibt zurueck, ob die Anwendung im Demo-Mode laeuft oder nicht
	 * 
	 * @return <code>true</code> - Die Anwendung laeuft im Demo-Mode
	 */
	public boolean isDemoMode() {
		return demoMode;
	}

	/**
	 * @return the sperreAktiv
	 */
	public boolean isSperreAktiv() {
		return sperreAktiv;
	}

	/**
	 * @param sperreAktiv the sperreAktiv to set
	 */
	public void setSperreAktiv(boolean sperreAktiv) {
		this.sperreAktiv = sperreAktiv;
	}

	/**
	 * Initialisierung des Fensters des TCO.<br>
	 * Setzt den Titel, das Layout (kein Layout), die Hintergrundfarbe und die Groesse.<br> 
	 * Das Fenster ist nicht skalierbar.<br>
	 * Die Anzeigeelemente werden ueber die <code>paint(Graphics)</code> Methode hinzugefuegt.
	 * Wenn die Anwendung nicht im Demo-Mode laeuft, werden die verwendeten DBD-Variablen abonniert.
	 * 
	 * @see ZDXFCTcoInitializer#initialize()
	 * @see ZDXFCTcoInitializer#abonniereElemente()
	 */
	private void initWindow() {
		addWindowListener(new ZDXFCWindowListener(dbd));
		
		setTitle(WINDOW_TITLE);
		setLayout(null);
		setBackground(WINDOW_BG_COLOR);
		setSize(WINDOW_SIZE_WIDTH, WINDOW_SIZE_HEIGHT);
		setResizable(false);

		initializer.initialize();
		
		setVisible(true);

		if (!demoMode) {
			initializer.abonniereElemente();
		}
	}
	
	/**
	 * Window-Listener zum geordneten Schliessen der Anwendung. Vor dem Beenden der Anwendung wird der DBD-Adapter
	 * gestoppt.<br>
	 * Der Listener reagiert nur auf das Event <code>windowClosing()</code>.
	 */
	private static class ZDXFCWindowListener extends WindowAdapter {
		private DBDAdapter dbd;

		public ZDXFCWindowListener(DBDAdapter dbd) {
			this.dbd = dbd;
		}
		
		@Override
		public void windowClosing(WindowEvent e) {
			if (dbd.isStarted()) {
				dbd.stop();
			}
			
			e.getWindow().dispose();
			System.exit(0);
		}
	}

	/**
	 * Verbindungsinformation des DBD-Adapters, wird beim Wechsel des Verbindungsstatus aufgerufen
	 * 
	 * @param observable Das Adapter-Objekt
	 * @param object Zustand der Verbindung (true, false)
	 */
	@Override
	public void update(Observable observable, Object object) {
		LOG.warn("Connected to DBD: " + object.toString());
	}
}
